--- title: "01-Spring Boot MVC 项目最佳实践规范" created: 2025-12-03 tags: - 项目 aliases: - Spring Boot MVC 项目最佳实践规范 --- # Spring Boot MVC 项目最佳实践规范 > 本文档整理了项目开发中的核心规范和常用代码模板,方便编码时快速查阅。 --- ## **一、命名规范** | **类型** | **命名格式** | **示例** | | --- | --- | --- | | Entity | `XxxEntity` 或 `Xxx` | `User`、`Team` | | DTO | `XxxDTO` 或 `XxxRequest` | `UserRegisterDTO` | | VO | `XxxVO` | `UserVO` | | Enum | `XxxEnum` | `GenderEnum` | | Service | `XxxService` | `UserService` | | ServiceImpl | `XxxServiceImpl` | `UserServiceImpl` | | Controller | `XxxController` | `UserController` | | Mapper | `XxxMapper` | `UserMapper` | --- ## **二、分层架构** ```text ┌─────────────┐ │ Controller │ → 参数接收、非空校验、触发DTO校验、调用Service ├─────────────┤ │ Service │ → 业务逻辑、业务校验、事务管理、调用Mapper ├─────────────┤ │ Mapper │ → 数据库操作(CRUD) ├─────────────┤ │ Entity │ → 数据库表映射 ├─────────────┤ │ DTO │ → 请求数据封装、字段校验 ├─────────────┤ │ VO │ → 响应数据封装、脱敏处理 └─────────────┘ ``` **职责边界**: - **Controller**:只做参数接收和结果返回,不写业务逻辑 - **Service**:核心业务逻辑,事务控制在此层 - **Mapper**:纯数据库操作,不含业务判断 --- ## **三、校验注解速查** | **注解** | **作用** | **示例** | | --- | --- | --- | | `@NotNull` | 不能为 null | `@NotNull` `Long id` | | `@NotBlank` | 字符串非空且非空白 | `@NotBlank` `String name` | | `@NotEmpty` | 集合/数组非空 | `@NotEmpty` `List tags` | | `@Size` | 长度/大小范围 | `@Size(min=1, max=10)` | | `@Min` / `@Max` | 数值范围 | `@Min(0)` `@Max(100)` | | `@Email` | 邮箱格式 | `@Email` `String email` | | `@Pattern` | 正则匹配 | `@Pattern(regexp="...")` | | `@AssertTrue` | 自定义校验方法 | `@AssertTrue isValid()` | | `@Valid` | 触发嵌套校验 | `@Valid AddressDTO address` | | `@Validated` | 分组校验 | `@Validated(UpdateGroup.class)` | **使用示例**: ```java @Data public class UserRegisterDTO { @NotBlank(message = "用户名不能为空") @Size(min = 2, max = 20, message = "用户名长度2-20字符") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 20, message = "密码长度6-20字符") private String password; @Email(message = "邮箱格式不正确") private String email; } ``` --- ## **四、枚举设计规范** ### **4.1 标准枚举模板** ```java @Getter @AllArgsConstructor public enum GenderEnum { MALE(0, "男"), FEMALE(1, "女"), UNKNOWN(2, "未知"); @EnumValue // MyBatis-Plus:存储到数据库的字段 private final Integer code; @JsonValue // Jackson:序列化到 JSON 的字段 private final String desc; /** * 根据 code 获取枚举(反序列化用) */ @JsonCreator public static GenderEnum fromCode(Integer code) { if (code == null) return null; for (GenderEnum e : values()) { if (e.code.equals(code)) return e; } return null; } /** * 校验 code 是否有效 */ public static boolean isValid(Integer code) { return fromCode(code) != null; } } ``` ### **4.2 枚举设计要点** | **注解** | **作用** | | --- | --- | | `@EnumValue` | 标记存储到数据库的字段 | | `@JsonValue` | 标记序列化到 JSON 的字段 | | `@JsonCreator` | 标记反序列化的工厂方法 | **必备方法**: - `fromCode()` - 根据 code 获取枚举 - `isValid()` - 验证 code 是否有效 --- ## **五、异常处理规范** ### **5.1 异常使用原则** | **做法** | **说明** | | --- | --- | | ✅ 定义专有 ErrorCode | 为不同业务错误定义不同的错误码 | | ✅ 添加上下文信息 | 抛出异常时提供具体信息 | | ✅ 使用 ThrowUtils | 简化异常抛出代码 | | ✅ 区分错误类型 | CLIENT(用户错误) vs SERVER(系统错误) | | ✅ 提前校验 | 在关键操作前进行参数和业务校验 | ### **5.2 正确示例** ```java // ✅ 好的做法:专有错误码 + 上下文信息 throw new BusinessException( ErrorCode.USER_NOT_FOUND, "User with id " + userId + " does not exist" ); // ✅ 好的做法:使用 ThrowUtils 简化 ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND); ThrowUtils.throwIf(amount <= 0, ErrorCode.PARAMS_ERROR, "金额必须大于0"); // ❌ 不好的做法:通用错误码,无上下文 throw new BusinessException(ErrorCode.ERROR); ``` ### **5.3 ThrowUtils 常用方法** ```java // 条件判断 ThrowUtils.throwIf(condition, ErrorCode.XXX); ThrowUtils.throwIf(condition, ErrorCode.XXX, "详细信息"); // 空值判断 ThrowUtils.throwIfNull(obj, ErrorCode.XXX); User user = ThrowUtils.throwIfNull(userMapper.selectById(id), ErrorCode.USER_NOT_FOUND); // 空字符串判断 ThrowUtils.throwIfBlank(str, ErrorCode.XXX); ``` --- ## **六、权限校验** ### **6.1 注解方式** | **注解** | **作用** | **示例** | | --- | --- | --- | | `@Anonymous` | 允许匿名访问 | `@Anonymous @GetMapping("/public")` | | `@RequiresPermission("xxx")` | 需要单个权限 | `@RequiresPermission("user:add")` | | `@RequiresPermission(value={"a","b"}, logical=Logical.OR)` | 任一权限即可 | 满足 a 或 b | | `@RequiresPermission(value={"a","b"}, logical=Logical.AND)` | 需要所有权限 | 同时有 a 和 b | | `@RequiresRole("xxx")` | 需要角色 | `@RequiresRole("admin")` | ### **6.2 获取用户信息** ```java // 可能为 null LoginUserVO user = SecurityUtils.getLoginUser(); Long userId = SecurityUtils.getUserId(); String username = SecurityUtils.getUsername(); // 必须有值,否则抛异常 LoginUserVO user = SecurityUtils.requireLoginUser(); Long userId = SecurityUtils.requireUserId(); ``` ### **6.3 权限校验(失败抛异常)** ```java SecurityUtils.checkPermission("user:add"); // 单权限 SecurityUtils.checkPermission("user:add", "user:edit"); // 必须同时有 SecurityUtils.checkAnyPermission("user:add", "user:edit"); // 有任一即可 SecurityUtils.checkRole("admin"); // 单角色 SecurityUtils.checkAnyRole("admin", "manager"); // 有任一即可 ``` ### **6.4 权限判断(返回 boolean)** ```java boolean has = SecurityUtils.hasPermission("user:add"); boolean hasAny = SecurityUtils.hasAnyPermission("user:add", "user:edit"); boolean isAdmin = SecurityUtils.isAdmin(); boolean isSuperAdmin = SecurityUtils.isSuperAdmin(); boolean isLoggedIn = SecurityUtils.isAuthenticated(); ``` ### **6.5 数据级权限** ```java // 只允许本人或管理员 SecurityUtils.checkSelfOrAdmin(userId); // 只允许资源所有者 SecurityUtils.checkOwner(ownerId); // 判断(不抛异常) boolean can = SecurityUtils.isSelfOrAdmin(userId); ``` ### **6.6 认证状态** ```java SecurityUtils.requireAuthenticated(); // 必须已登录 SecurityUtils.requireNotAuthenticated(); // 必须未登录(用于登录接口) ``` --- ## **七、配置管理** ### **7.1 配置层级** ```text 优先级从低到高: application.yml → application-{profile}.yml → .env.{profile} ``` ### **7.2 环境变量映射规则** ```text YAML 配置 → 环境变量 app.name → APP_NAME app.jwt.secret → APP_JWT_SECRET app.file.max-size → APP_FILE_MAX_SIZE ``` ### **7.3 注入 AppProperties** | **方式** | **代码** | **适用场景** | | --- | --- | --- | | **构造器注入**(推荐) | `public MyService(AppProperties appProperties)` | Service、Controller | | 字段注入 | `@Autowired` `private AppProperties appProperties;` | 简单场景 | | 方法参数 | `public void method(AppProperties config)` | 特殊场景 | ```java // ✅ 推荐:构造器注入 @Service public class AuthService { private final AppProperties appProperties; public AuthService(AppProperties appProperties) { this.appProperties = appProperties; } } ``` ### **7.4 配置项速查表** | **获取方式** | **返回类型** | **说明** | | --- | --- | --- | | `appProperties.getName()` | `String` | 应用名称 | | `appProperties.getVersion()` | `String` | 应用版本 | | `appProperties.isDebug()` | `boolean` | 是否调试模式 | | `appProperties.isProduction()` | `boolean` | 是否生产环境 | | `appProperties.isDevelopment()` | `boolean` | 是否开发环境 | #### **JWT 配置** `appProperties.getJwt()` | **方法** | **返回类型** | **说明** | | --- | --- | --- | | `.getSecret()` | `String` | JWT 密钥 | | `.getExpiration()` | `long` | 过期时间(秒) | | `.getExpirationMs()` | `long` | 过期时间(毫秒) | | `.getTokenPrefix()` | `String` | Token 前缀,默认 `Bearer` | | `.getHeaderName()` | `String` | Header 名称,默认 `Authorization` | #### **安全配置** `appProperties.getSecurity()` | **方法** | **返回类型** | **说明** | | --- | --- | --- | | `.getPasswordSalt()` | `String` | 密码静态盐值 | | `.getBcryptStrength()` | `int` | BCrypt 强度(4-31) | #### **文件配置** `appProperties.getFile()` | **方法** | **返回类型** | **说明** | | --- | --- | --- | | `.getMaxSize()` | `long` | 最大文件大小(字节) | | `.getMaxSizeMB()` | `long` | 最大文件大小(MB) | | `.getAllowedFormats()` | `String` | 允许格式(逗号分隔) | | `.getAllowedFormatArray()` | `String[]` | 允许格式(数组) | | `.getUploadPath()` | `String` | 上传路径 | #### **用户配置** `appProperties.getUser()` | **方法** | **返回类型** | **说明** | | --- | --- | --- | | `.getMaxPasswordRetry()` | `int` | 密码最大重试次数 | | `.getMaxLoginDevice()` | `int` | 最大同时登录设备数 | | `.getLockMinutes()` | `int` | 账户锁定时间(分钟) | #### **CORS 配置** `appProperties.getCors()` | **方法** | **返回类型** | **说明** | | --- | --- | --- | | `.getAllowedOrigins()` | `String` | 允许的源(逗号分隔) | | `.getAllowedOriginsArray()` | `String[]` | 允许的源(数组) | ### **7.5 使用示例** ```java // JWT 相关 String secret = appProperties.getJwt().getSecret(); long expirationMs = appProperties.getJwt().getExpirationMs(); // 文件校验 long maxSize = appProperties.getFile().getMaxSize(); if (file.getSize() > maxSize) { throw new BusinessException("文件不能超过 " + appProperties.getFile().getMaxSizeMB() + "MB"); } // 环境判断 if (appProperties.isProduction()) { // 生产环境逻辑 } // 密码加密 String salt = appProperties.getSecurity().getPasswordSalt(); int strength = appProperties.getSecurity().getBcryptStrength(); // CORS 配置 String[] origins = appProperties.getCors().getAllowedOriginsArray(); ``` ### **7.6 文件清单** | **文件** | **用途** | **是否提交 Git** | | --- | --- | --- | | `application.yml` | 主配置,所有默认值 | ✅ | | `application-dev.yml` | 开发环境覆盖 | ✅ | | `application-prod.yml` | 生产环境覆盖 | ✅ | | `.env.example` | 环境变量模板 | ✅ | | `.env.dev` | 开发环境变量 | ❌ | | `.env.prod` | 生产环境变量 | ❌ | --- ## **八、API文档规范** ### **8.1 Controller 注解** ```java @RestController @RequestMapping("/user") @Tag(name = "用户管理", description = "用户相关接口") public class UserController { @GetMapping("/{id}") @Operation(summary = "获取用户详情", description = "根据ID获取用户信息") public Result getById( @Parameter(description = "用户ID") @PathVariable Long id) { // ... } } ``` ### **8.2 DTO/VO 注解** ```java @Data @Schema(description = "用户注册请求") public class UserRegisterDTO { @Schema(description = "用户名", example = "zhangsan") @NotBlank(message = "用户名不能为空") private String username; @Schema(description = "密码", example = "123456") @NotBlank(message = "密码不能为空") private String password; } ``` ### **8.3 生产环境禁用** ```yaml knife4j: enable: ${KNIFE4J_ENABLE:false} ``` --- ## **九、快速检查清单** ### **开发前检查** - 是否定义了合适的 ErrorCode - DTO 是否添加了校验注解 - 枚举是否包含 `@EnumValue`、`@JsonValue`、`fromCode()` ### **开发中检查** - Controller 是否只做参数接收和结果返回 - Service 是否添加了 `@Transactional`(写操作) - 是否使用 ThrowUtils 进行参数校验 - 是否进行了数据级权限校验 ### **提交前检查** - 敏感配置是否在 `.env` 文件中 - `.env` 文件是否在 `.gitignore` 中 - API 文档注解是否完整 - 生产环境是否禁用了 Knife4j ## **十、启动与部署** ### **10.1 环境文件说明** | **文件** | **用途** | **提交 Git** | | --- | --- | --- | | `.env.example` | 环境变量模板,包含所有配置项 | ✅ | | `.env.dev` | 开发环境配置 | ❌ | | `.env.prod` | 生产服务器配置 | ❌ | | `.env.prod.local` | 本地模拟生产环境(连接测试库等) | ❌ | **首次使用**:复制模板并填写配置 ```bash cp .env.example .env.dev cp .env.example .env.prod cp .env.example .env.prod.local ``` ### **10.2 本地开发启动** #### **Windows(推荐使用启动脚本)** ```text 项目根目录/ ├── start-dev.bat # 开发环境启动 └── start-prod.bat # 本地模拟生产环境启动 ``` **双击运行** `start-dev.bat` 或在命令行: ```dos .\start-dev.bat ``` #### **IDEA 启动配置** 1. **Edit Configurations** → **Add New** → **Spring Boot** 2. 配置如下: | **配置项** | **值** | | --- | --- | | Main class | `com.zwnsyw.zwwwspringbootbasetemplate.ZwwwSpringBootBaseTemplateApplication` | | Active profiles | `dev` | | Environment variables | 从 `.env.dev` 复制,或使用 EnvFile 插件 | **手动设置环境变量**(IDEA): ```properties APP_JWT_SECRET=your-secret-key;APP_SECURITY_PASSWORD_SALT=your-salt;DB_PASSWORD=xxx ``` **使用 EnvFile 插件**(推荐): 1. 安装插件:File → Settings → Plugins → 搜索 "EnvFile" 2. Run Configuration → EnvFile 标签 → 勾选 Enable → 添加 `.env.dev` #### **Maven 命令启动** ```bash # Windows - 需要先手动加载环境变量,或使用脚本 mvn spring-boot:run -Dspring-boot.run.profiles=dev # Linux/Mac export $(cat .env.dev | grep -v '^#' | xargs) && mvn spring-boot:run -Dspring-boot.run.profiles=dev ``` ### **10.3 服务器部署** #### **10.3.1 打包** ```bash # 跳过测试打包 mvn clean package -DskipTests # 生成文件 target/zwww-springboot-base-template-0.0.1-SNAPSHOT.jar ``` #### **10.3.2 上传部署文件** ```bash # 上传到服务器 scp target/*.jar user@server:/opt/app/ scp .env.prod user@server:/opt/app/.env scp deploy.sh user@server:/opt/app/ ``` #### **10.3.3 服务器目录结构** ```text /opt/app/ ├── zwww-springboot-base-template.jar # 应用 jar ├── .env # 环境变量文件 ├── deploy.sh # 部署脚本 ├── logs/ # 日志目录 │ ├── app.log # 应用日志 │ └── error.log # 错误日志 └── backup/ # 备份目录 ``` #### **10.3.4 部署脚本** `deploy.sh` ```bash #!/bin/bash # ============================================ # Spring Boot 应用部署脚本 # 用法: ./deploy.sh [start|stop|restart|status] # ============================================ APP_NAME="zwww-springboot-base-template" APP_JAR="${APP_NAME}.jar" APP_DIR="/opt/app" LOG_DIR="${APP_DIR}/logs" ENV_FILE="${APP_DIR}/.env" PID_FILE="${APP_DIR}/${APP_NAME}.pid" # JVM 参数 JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC" # 确保日志目录存在 mkdir -p ${LOG_DIR} # 加载环境变量 load_env() { if [ -f "${ENV_FILE}" ]; then echo "加载环境变量: ${ENV_FILE}" export $(cat ${ENV_FILE} | grep -v '^#' | grep -v '^$' | xargs) else echo "[错误] 环境变量文件不存在: ${ENV_FILE}" exit 1 fi } # 获取 PID get_pid() { if [ -f "${PID_FILE}" ]; then cat ${PID_FILE} else echo "" fi } # 检查是否运行中 is_running() { local pid=$(get_pid) if [ -n "${pid}" ] && ps -p ${pid} > /dev/null 2>&1; then return 0 else return 1 fi } # 启动 start() { if is_running; then echo "[警告] ${APP_NAME} 已在运行中 (PID: $(get_pid))" return 1 fi load_env echo "启动 ${APP_NAME}..." cd ${APP_DIR} nohup java ${JAVA_OPTS} \ -Dspring.profiles.active=prod \ -jar ${APP_JAR} \ > ${LOG_DIR}/app.log 2>&1 & echo $! > ${PID_FILE} sleep 3 if is_running; then echo "[成功] ${APP_NAME} 已启动 (PID: $(get_pid))" else echo "[错误] ${APP_NAME} 启动失败,请检查日志" cat ${LOG_DIR}/app.log | tail -50 return 1 fi } # 停止 stop() { if ! is_running; then echo "[信息] ${APP_NAME} 未运行" return 0 fi local pid=$(get_pid) echo "停止 ${APP_NAME} (PID: ${pid})..." kill ${pid} # 等待进程结束(最多30秒) local count=0 while is_running && [ ${count} -lt 30 ]; do sleep 1 count=$((count + 1)) echo -n "." done echo "" if is_running; then echo "[警告] 进程未响应,强制终止..." kill -9 ${pid} fi rm -f ${PID_FILE} echo "[成功] ${APP_NAME} 已停止" } # 重启 restart() { stop sleep 2 start } # 状态 status() { if is_running; then echo "[运行中] ${APP_NAME} (PID: $(get_pid))" else echo "[已停止] ${APP_NAME}" fi } # 查看日志 logs() { tail -f ${LOG_DIR}/app.log } # 主入口 case "$1" in start) start ;; stop) stop ;; restart) restart ;; status) status ;; logs) logs ;; *) echo "用法: $0 {start|stop|restart|status|logs}" exit 1 ;; esac ``` #### **10.3.5 部署命令速查** | **操作** | **命令** | | --- | --- | | 启动 | `./deploy.sh start` | | 停止 | `./deploy.sh stop` | | 重启 | `./deploy.sh restart` | | 状态 | `./deploy.sh status` | | 查看日志 | `./deploy.sh logs` | | 实时日志 | `tail -f /opt/app/logs/app.log` | ### **10.4 Docker 部署** #### **Dockerfile** ```dockerfile FROM openjdk:17-jdk-slim WORKDIR /app COPY target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=prod"] ``` #### **构建与运行** ```bash # 构建镜像 docker build -t zwww-app:latest . # 运行容器 docker run -d \ --name zwww-app \ --env-file .env.prod \ -p 8080:8080 \ zwww-app:latest # 查看日志 docker logs -f zwww-app ``` ### **10.5 常见问题** | **问题** | **原因** | **解决方案** | | --- | --- | --- | | 环境变量未生效 | 未正确加载 .env 文件 | 使用启动脚本或 EnvFile 插件 | | 端口被占用 | 8080 端口已使用 | `netstat -ano | findstr 8080` 查找并结束进程 | | 启动后立即退出 | 配置错误 | 查看日志 `logs/app.log` | | 数据库连接失败 | 配置或网络问题 | 检查 DB\_HOST、DB\_PORT、防火墙 | --- ## **附录:项目结构** ```text src/main/java/com/zwnsyw/zwwwspringbootbasetemplate/ ├── ZwwwSpringBootBaseTemplateApplication.java # 启动类 │ ├── config/ # 配置类 │ ├── AppProperties.java # 应用配置属性 │ ├── MyBatisPlusConfig.java # MyBatis-Plus 配置 │ ├── Knife4jConfig.java # Knife4j API文档配置 │ └── CorsConfig.java # 跨域配置 │ ├── controller/ # 控制器层 │ ├── UserController.java │ └── ConfigController.java │ ├── service/ # 服务层接口 │ └── UserService.java │ ├── service/impl/ # 服务层实现 │ └── UserServiceImpl.java │ ├── mapper/ # MyBatis Mapper 接口 │ └── UserMapper.java │ ├── model/ # 模型层 │ ├── entity/ # 数据库实体 │ │ └── User.java │ ├── dto/ # 数据传输对象(请求) │ │ ├── UserDTO.java │ │ └── user/ │ │ ├── UserRegisterDTO.java │ │ ├── UserLoginDTO.java │ │ └── UserUpdateDTO.java │ ├── vo/ # 视图对象(响应) │ │ └── UserVO.java │ ├── query/ # 查询对象 │ │ └── UserQueryDTO.java │ └── enums/ # 枚举类 │ ├── GenderEnum.java │ ├── UserStatusEnum.java │ └── UserRoleEnum.java │ ├── common/ # 公共模块 │ ├── BaseResponse.java # 统一响应体 │ ├── ErrorCode.java # 错误码枚举 │ ├── ResultUtils.java # 响应工具类 │ └── PageResult.java # 分页结果 │ ├── exception/ # 异常处理 │ ├── BusinessException.java # 业务异常 │ └── GlobalExceptionHandler.java # 全局异常处理器 │ ├── security/ # 安全模块 │ ├── annotation/ # 注解定义 │ │ ├── Anonymous.java # 匿名访问 │ │ ├── RequiresPermission.java # 权限校验 │ │ └── RequiresRole.java # 角色校验 │ ├── config/ # 安全配置 │ │ ├── SecurityConfig.java # 安全配置 │ │ └── AnonymousUrlConfig.java # 匿名URL配置 │ ├── context/ # 安全上下文 │ │ └── SecurityContext.java # 当前用户上下文 │ ├── enums/ # 枚举 │ │ └── Logical.java # 逻辑符枚举 │ ├── handler/ # 权限处理器 │ │ └── PermissionHandler.java # 权限校验逻辑 │ ├── interceptor/ # 拦截器 │ │ └── AuthorizationInterceptor.java # 鉴权拦截器 │ └── utils/ # 安全工具类 │ └── SecurityUtils.java │ ├── utils/ # 工具类 │ └── PasswordUtils.java # 密码工具类 │ └── validation/ # 自定义校验器 └── groups/ └── UpdateGroup.java # 更新分组 src/main/resources/ ├── application.yml # 主配置文件 ├── application-dev.yml # 开发环境配置 ├── application-prod.yml # 生产环境配置 └── mapper/ # MyBatis XML 映射文件 └── UserMapper.xml db/ └── init.sql # 数据库初始化脚本 # 环境变量文件与启动脚本(根目录) 项目根目录/ ├── start-dev.bat # 开发环境启动 ├── start-prod.bat # 本地模拟生产环境启动 ├── .env.example # 环境变量模板(提交Git) ├── .env.dev # 开发环境变量(不提交) ├── .env.prod # 生产环境变量(不提交) └── .env.prod.local # 本地模拟生产环境(不提交) ``` --- **项目分区导航**:⬅️ [[00-最佳实践|00-最佳实践]] | 01-Spring Boot MVC 项目最佳实践规范 | ➡️ [[02-SpringBoot MVC 分层最佳实践|02-SpringBoot MVC 分层最佳实践]]